/*
 * Copyright 2007 Matt Jensen
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.jtell.internal;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jtell.EventSink;
import org.jtell.config.EventSinkMetadata;
import org.jtell.config.OrderMetadata;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * <code>EventNotifierImpl</code> is the concrete implementation of the {@link EventNotifier} interface.
 * </p>
 */
public class EventNotifierImpl implements EventNotifier
{
    private static Log LOG = LogFactory.getLog(EventNotifierImpl.class);

    /**
     * <p>
     * Construct a {@link EventNotifierImpl} instance.
     * </p>
     */
    public EventNotifierImpl()
    {
        super();
    }

    /* inherit javadoc */
    public void notifyEventSinks(final Collection<EventSink> eventSinks, final Object event)
    {
        // Nothing to do unless we get at least one event sink.
        final int eventSinksSize = eventSinks.size();
        if (0 == eventSinksSize)
        {
            if (LOG.isDebugEnabled())
            {
                LOG.debug(String.format("Event sink set for event [%s] is empty.", eventSinks));
            }
        }
        else
        {
            // First we need to get all tokens contributed by all event sinks. Note that we can skip the scan if we have
            // exactly one event sink because its contributions would have no impact on its own invocation.
            final Map<String, Boolean> contributionBitmap;
            if (1 == eventSinksSize)
            {
                contributionBitmap = EMPTY_CONTRIBUTION_BITMAP;
            }
            else
            {
                // Build the contribution bitmap, which maps each contribution token to a Boolean value indicating
                // whether it has yet been contributed by an event sink (since no event sinks have been invoked yet,
                // these values will all be initialized to FALSE.)
                contributionBitmap = new HashMap<String, Boolean>();
                for (final EventSink eventSink : eventSinks)
                {
                    final EventSinkMetadata metadata = eventSink.getEventSinkMetadata();
                    if (metadata.isContributor())
                    {
                        for (final String token : metadata.getContributions())
                        {
                            contributionBitmap.put(token, Boolean.FALSE);
                        }
                    }
                }
            }

            // Notify the event sinks.
            for (final EventSink eventSink : eventSinks)
            {
                notifyEventSink(eventSink, event, contributionBitmap);
            }
        }
    }

    /**
     * <p>
     * Notify an event sink of an event, verifying that all required <em>before</em> and <em>after</em> contributions
     * are honored.
     * </p>
     *
     * @param eventSink the event sink to notify.
     * @param event the event.
     * @param contributionBitmap the contribution status bitmap.
     */
    private void notifyEventSink(final EventSink eventSink,
                                 final Object event,
                                 final Map<String, Boolean> contributionBitmap)
    {
        // Examine order metadata to determine whether this event sink should be skipped because either a) a
        // required-after token has not been contributed by another event sink; or b) a required-before token has
        // already been contributed by another event sink or is never contributed by any event sink.
        boolean skip = false;
        final EventSinkMetadata metadata = eventSink.getEventSinkMetadata();
        if (metadata.isConditional())
        {
            // Check for unsatisfied required-after orders.
            for (final OrderMetadata order : metadata.getAfterOrders())
            {
                if (order.isRequired())
                {
                    final Boolean contributed = contributionBitmap.get(order.getToken());
                    if (null == contributed)
                    {
                        skip = true;
                        if (LOG.isDebugEnabled())
                        {
                            LOG.debug(String.format(
                                    "Skipping notification of event [%s] for event sink [%s] because required-after token [%s] is not supplied by any matching event sink.",
                                    event, eventSink, order.getToken()));
                        }
                    }
                    else if (!contributed)
                    {
                        skip = true;
                        if (LOG.isDebugEnabled())
                        {
                            LOG.debug(String.format(
                                    "Skipping notification of event [%s] for event sink [%s] because required-after token [%s] has not been contributed by any previous matching event sink.",
                                    event, eventSink, order.getToken()));
                        }
                    }
                }
            }
            if (!skip)
            {
                // Check for unsatisfied required-before orders.
                for (final OrderMetadata order : metadata.getBeforeOrders())
                {
                    if (order.isRequired())
                    {
                        final Boolean contributed = contributionBitmap.get(order.getToken());
                        if (null == contributed)
                        {
                            skip = true;
                            if (LOG.isDebugEnabled())
                            {
                                LOG.debug(String.format(
                                        "Skipping notification of event [%s] for event sink [%s] because required-before token [%s] is not supplied by any matching event sink.",
                                        event, eventSink, order.getToken()));
                            }
                        }
                        else if (contributed)
                        {
                            // TODO: this is really an error--something was sorted incorrectly
                            skip = true;
                            if (LOG.isDebugEnabled())
                            {
                                LOG.debug(String.format(
                                        "Skipping notification of event [%s] for event sink [%s] because required-before token [%s] has been contributed by a previous matching event sink.",
                                        event, eventSink, order.getToken()));
                            }
                        }
                    }
                }
            }

            // TODO: also check attributes. Probably first as that should be faster?
        }
        if (!skip)
        {
            // Notify the event sink.
            eventSink.notifyListener(event);

            // Update the contribution bitmap.
            if (metadata.isContributor())
            {
                for (final String token : metadata.getContributions())
                {
                    contributionBitmap.put(token, Boolean.TRUE);
                }
            }
        }
    }

    /**
     * <p>
     * Empty contribution bitmap.
     * </p>
     */
    private static final Map<String, Boolean> EMPTY_CONTRIBUTION_BITMAP = Collections.emptyMap();
}
